#!/usr/bin/env python3
# 
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#

import curses
import curses.ascii
from typing import Mapping

from qiling import Qiling

COLORS_MAPPING = {
    0: curses.COLOR_BLACK,
    1: curses.COLOR_BLUE,
    2: curses.COLOR_GREEN,
    3: curses.COLOR_CYAN,
    4: curses.COLOR_RED,
    5: curses.COLOR_MAGENTA,
    6: 9,
    7: 7,
    8: 8,
    9: 6,
    10: 10,
    11: 14,
    12: 9,
    13: 13,
    14: curses.COLOR_YELLOW,
    15: curses.COLOR_WHITE
}

REVERSE_COLORS_MAPPING = {v : k for k, v in COLORS_MAPPING.items()}

def get_attr(color_pairs: Mapping[int, Mapping[int, int]], char: int) -> int:
    fg = (char & 0x0f)
    bg = (char & 0xf0) >> 4

    # For blinking
    attr = color_pairs[fg][bg & 0b0111]

    if (bg & 0b1000) != 0:
        attr |= curses.A_BLINK

    return attr

def get_ch_non_blocking(scr) -> int:
    scr.timeout(0)
    key = scr.getch()
    scr.timeout(-1)

    return key

def __leaf_00(ql: Qiling):
    # time to set up curses
    # copied from curses.wrapper

    stdscr = curses.initscr()
    curses.noecho()
    curses.cbreak()
    stdscr.keypad(True)

    try:
        curses.start_color()
    except:
        pass

    al = ql.arch.regs.al

    resolution = {
        0x00 : (25, 40),
        0x01 : (25, 40),
        0x02 : (25, 80),
        0x03 : (25, 80),
        0x04 : (200, 320),
        0x05 : (200, 320),
        0x06 : (200, 640),
        0x08 : (200, 160),
        0x09 : (200, 320),
        0x0a : (200, 640),
        0x0d : (200, 320),
        0x0e : (200, 640),
        0x0f : (350, 640),
        0x10 : (350, 640),
        0x11 : (480, 640),
        0x12 : (480, 640),
        0x13 : (200, 320)
    }.get(al)

    if resolution is None:
        ql.log.exception(f'resolution not implemented (al: {al:#02x})')
        raise NotImplementedError()

    curses.resizeterm(*resolution)

    # Quoted from https://linux.die.net/man/3/resizeterm
    #
    # If ncurses is configured to supply its own SIGWINCH handler, 
    # the resizeterm function ungetch's a KEY_RESIZE which will be 
    # read on the next call to getch.
    ch = get_ch_non_blocking(stdscr)

    if ch == curses.KEY_RESIZE:
        ql.log.info(f'terminal has been resized')
    elif ch != -1:
        curses.ungetch(ch)

    stdscr.scrollok(True)

    if not curses.has_colors():
        ql.log.warning(f'your terminal does not support colors, content might not be displayed correctly')

    # https://en.wikipedia.org/wiki/BIOS_color_attributes
    # blink support?
    if curses.has_colors():
        for fg in range(16):
            for bg in range(16):
                color_pair_index = 16 * fg + bg + 1

                if fg not in ql.os.color_pairs:
                    ql.os.color_pairs[fg] = {}

                curses.init_pair(color_pair_index, COLORS_MAPPING[fg], COLORS_MAPPING[bg])
                color_pair = curses.color_pair(color_pair_index)

                ql.os.color_pairs[fg][bg] = color_pair
                ql.os.revese_color_pairs[color_pair] = (fg, bg)

    ql.os.stdscr = stdscr

def __leaf_01(ql: Qiling):
    # limited support
    ch = ql.arch.regs.ch

    if (ch & 0x20):
        curses.curs_set(0)

def __leaf_02(ql: Qiling):
    # page number ignored
    dh = ql.arch.regs.dh    # row
    dl = ql.arch.regs.dl    # column

    ql.os.stdscr.move(dh, dl)

def __leaf_05(ql: Qiling):
    # No idea how to implement, do nothing here.
    ql.arch.regs.al = 0

def __leaf_06(ql: Qiling):
    stdscr = ql.os.stdscr

    al = ql.arch.regs.al    # lines to scroll
    ch = ql.arch.regs.ch    # row of upper-left cornner
    cl = ql.arch.regs.cl    # column of upper-left corner
    dh = ql.arch.regs.dh    # row of lower right corner
    dl = ql.arch.regs.dl    # column of lower righ corner
    bh = ql.arch.regs.bh    # color

    y, x = stdscr.getmaxyx()
    cy, cx = stdscr.getyx()
    attr = get_attr(ql.os.color_pairs, bh)

    if ch != 0 or cl != 0 or dh != y - 1 or dl != x - 1:
        ql.log.warning(f'Partial scroll is unsupported. Will scroll the whole page.')
        ql.log.warning(f'Resolution: {y}x{x} but asked to scroll [({ch},{cl}), ({dh}, {dl})]')

    if al == 0:
        stdscr.clear()

        # Alternate way?
        #for ln in range(y):
        #    stdscr.addstr(ln, 0, " " * x, attr)

        stdscr.bkgd(" ", attr)
        stdscr.move(0, 0)

    else:
        stdscr.scroll(al)
        ny = 0

        if cy - al < 0:
            ny = 0
        else:
            ny = cy - al + 1

        if al > y:
            al = y

        for ln in range(al):
            stdscr.addstr(ny + ln, 0, " " * x, attr)

        stdscr.move(cy, cx)

def __leaf_08(ql: Qiling):
    stdscr = ql.os.stdscr

    if stdscr is None:
        ql.arch.regs.ax = 0x0720
    else:
        cy, cx = stdscr.getyx()
        inch = stdscr.inch(cy, cx)
        attr = inch & curses.A_COLOR
        ch = inch & 0xFF
        ql.arch.regs.al = ch
        pair_number = curses.pair_number(attr)

        fg, bg = curses.pair_content(pair_number)
        orig_fg = REVERSE_COLORS_MAPPING[fg]
        orig_bg = REVERSE_COLORS_MAPPING[bg]

        if attr & curses.A_BLINK:
            orig_bg |= 0b1000

        ql.arch.regs.ah = ((orig_bg << 4) & orig_fg)

def __leaf_0e(ql: Qiling):
    al = ql.arch.regs.al

    ql.log.debug(f'echo: {al:02x} -> {curses.ascii.unctrl(al)}')

    stdscr = ql.os.stdscr
    cy, cx = stdscr.getyx()

    # https://stackoverflow.com/questions/27674158/how-to-get-color-information-with-mvinch
    # https://linux.die.net/man/3/inch
    # https://github.com/mirror/ncurses/blob/master/include/curses.h.in#L1197
    # wtf curses...

    if al == 0xa:
        y, x = stdscr.getmaxyx()

        # \n will erase current line with echochar, so we have to handle it carefully.
        ql.log.info(f"Resolution: {x}x{y}, Cursor position: {cx},{cy}, Going to get a new line.")

        if (y - 1) == cy:
            # scroll doesn't affect our cursor
            stdscr.scroll(1)
            stdscr.move(cy, 0)
        else:
            stdscr.move(cy + 1, 0)
    else:
        attr = stdscr.inch(cy, cx) & curses.A_COLOR

        stdscr.echochar(al, attr)


# BIOS video support
# https://en.wikipedia.org/wiki/INT_10H
# https://stanislavs.org/helppc/idx_interrupt.html
# implemented by curses
def handler(ql: Qiling):
    ah = ql.arch.regs.ah

    leaffunc = {
        0x00 : __leaf_00,
        0x01 : __leaf_01,
        0x02 : __leaf_02,
        0x05 : __leaf_05,
        0x06 : __leaf_06,
        0x08 : __leaf_08,
        0x0e : __leaf_0e
    }.get(ah)

    if leaffunc is None:
        ql.log.exception(f'leaf {ah:02x}h of INT 10h is not implemented')
        raise NotImplementedError()

    leaffunc(ql)

    if ql.os.stdscr is not None:
        ql.os.stdscr.refresh()
